﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace DecisionTree
{
    class DecisionTree
    {
        public Forest Owner { get; private set; }

        public byte[][] inputs { get; private set; }
        public byte[] outputs { get; private set; }
        
        private int[] indexes;
        private int numOutputClasses;

        private List<DecisionNode> nodeJobList = new List<DecisionNode>();

        public int[] NonZeroAttributes { get; set; }

        public int InputLength { get; private set; }    

        /// <summary>
        ///   The train error of this decision tree.
        /// </summary>
        public int TrainError { get; private set; }

        /// <summary>
        ///   The test error of this decision tree.
        /// </summary>
        public int TestError { get; private set; }

        /// <summary>
        ///   If this tree is in a forest, this value is the index of the tree, otherwise null.
        /// </summary>
        public int? TreeIndex { get; private set; }

        /// <summary>
        ///   Gets the root node for this tree.
        /// </summary>
        public DecisionNode Root { get; private set; }

        public string LogString { get; private set; }

        public DecisionTree(byte[][] inputs, byte[] outputs, int numOutputClasses, int? treeIndex = null, int[] indexes = null, Forest owner = null)
        {
            this.inputs = inputs;
            this.outputs = outputs;
            this.indexes = indexes;
            this.numOutputClasses = numOutputClasses;
            this.TreeIndex = treeIndex;
            this.InputLength = inputs[0].Length;
            this.Owner = owner;

            if (indexes == null)
                indexes = Tools.CreateIndexes(inputs.Length);

            // 1. Create a root node for the tree
            this.Root = new DecisionNode(this, null, indexes);

            nodeJobList.Add(this.Root);

            FillTree();

            this.TrainError = Test(inputs, outputs, indexes);
            this.TestError = Test(this.Owner.testInputs, this.Owner.testOutputs);

            this.Log();
        }

        private void FillTree()
        {
            while (this.nodeJobList.Count > 0)
            {
                DecisionNode currentNode = this.nodeJobList[0];

                currentNode.Split();
                if (currentNode.Branches != null && currentNode.Branches.Length > 0)
                {
                    this.nodeJobList.Add(currentNode.Branches[0]);
                    this.nodeJobList.Add(currentNode.Branches[1]);
                }

                this.nodeJobList.RemoveAt(0);
            }
        }
        
        public int Test(byte[][] inputs, byte[] outputs, int[] idxs = null)
        {
            int miss = 0;

            if (idxs == null)
                idxs = Tools.CreateIndexes(inputs.Length);

            for (int i = 0; i < idxs.Length; i++)
                if (this.Compute(inputs[idxs[i]]) != outputs[idxs[i]])
                    miss++;

            return miss;
        }       

        public byte Compute(byte[] p)
        {
            DecisionNode currentNode = this.Root;

            while (!currentNode.IsLeaf)
            {
                if (currentNode.IsValid(p))
                    currentNode = currentNode.Branches[0];
                else
                    currentNode = currentNode.Branches[1];
            }

            return (byte)currentNode.Output.Value;
        }

        public double[] ComputeWithHistogram(byte[] p)
        {
            DecisionNode currentNode = this.Root;

            while (!currentNode.IsLeaf)
            {
                if (currentNode.IsValid(p))
                    currentNode = currentNode.Branches[0];
                else
                    currentNode = currentNode.Branches[1];
            }

            return currentNode.OutputHistogram;
        }        

        public void Log()
        {
            List<int> leafDistances = TreeStatistics.GetLeafDistances(this);

            double average = leafDistances.Average();
            double min = leafDistances.Min();
            double max = leafDistances.Max();
            double stdVar = leafDistances.StandardDeviation();

            double trainAccuracy = ( Convert.ToDouble( this.indexes.Length - this.TrainError )  / this.indexes.Length ) * 100.0;
            double testAccuracy = ( Convert.ToDouble(Parameters.TotalRecordsTesting - this.TestError) / Parameters.TotalRecordsTesting) * 100.0;

            LogString = "";
            LogString += this + " statistics:" + Environment.NewLine;
            LogString += "----------------" + Environment.NewLine;
            LogString += "Train error: " + this.TrainError + "( " + trainAccuracy + "% Accuracy )" + Environment.NewLine;
            LogString += "Test error: " + this.TestError + "( " + testAccuracy + "% Accuracy )" + Environment.NewLine;
            LogString += "Average path: " + average + Environment.NewLine;
            LogString += "Min path: " + min + Environment.NewLine;
            LogString += "Max path: " + max + Environment.NewLine;
            LogString += "Standard deviation path: " + stdVar;
        }

        public override string ToString()
        {
            if (this.TreeIndex.HasValue)
                return "Tree " + this.TreeIndex.ToString();
            return "One tree";
        } 

        public LightDecisionTree ToLightDecisionTree()
        {
            return new LightDecisionTree(this.InputLength, this.TrainError, this.TestError, this.TreeIndex, this.Root, this.LogString);
        }
    }
}


